Clean Architecture 是由 Uncle Bob(Robert C. Martin)提出的架構,是以 Clean Code 為基礎延伸出的架構設計。
Clean Architecture 的核心觀念是讓系統中的商業邏輯是獨立的:
Uncle Bob 提出的
Clean Architecture概念圖

我更喜歡以 PJ 大在 Go Clean Architectur 文章內的圖來理解:

這張圖主要定義幾個 Layer:
- Entities:類似 Model 層,又稱 Domain。定義 model 的 struct、以及透過
interface 來定義將被實作的方法。這些定義好的 entities 會在其他不同層中被使用。- Repository:類似 database 層,負責處理 database 資料的操作,只會對資料庫進行 CRUD 而不會有任何商業邏輯在內,因為在 Domain 層有定義 interface,所以切換 database 也沒關係。這一層會相依於其他資料庫或 micro service(用來交換資料)的服務。
- Usecase:類似 API 或 controller 層,是主要用來處理商業邏輯的(資料處理或運算)。在這層中會決定要使用哪一個 repository,並將資料交給 delivery 或 repository,負責使用 Repository 提供的方法來實際對 database 進行操作,因為 Domain 層有定義 interface,所以可以套用在不同的服務上(例如,gRPC)
- Delivery:類似 router 層,最主要用來決定資料要透過哪種媒介呈現,可以是 Restful API, gRPC 或 HTML 檔案。這層會接受使用者傳遞的資料,並消毒過濾(sanitize)後再往後傳給 usecase 層。
Entities 就是傳送/顯示的資料,比如說我們前幾章使用的 Member 就是 Entities Layer 的結構。
Repository 簡單來說就是資料 CRUD 的邏輯。
但跟我們前幾章 SQLite 不同的是,這邊的取得資料需要以 Protocol 定義,因為會需要是獨立於資料庫的。因此如何實作並不是重點,可以從資料庫取得資料,也可以從網路取得資料。
Usecase/Service 是主要商業邏輯的地方。
比方說取得資料後,怎麼處理資料,最後傳給 UI,就是這邊處理。
Delivery/Controller 對於 SwiftUI 來說就是 UI 或 UI 相關的數值。
根據 Clean Architecture 衍生出的 SwiftUI Clean Architecture 結構可以參考 nalexn: Clean Architecture for SwiftUI 這篇文章。
下圖是 nalexn 提出的
SwiftUI Clean Architecture示意圖

上圖分為四個模組,對應 Clean Architecture Layer 分別為:
Repository 負責處理 CRUD 的邏輯。
Interactor 處理主要商業邏輯,主要以業務類型來分類,不同的業務邏輯會有不同的 Interactor。
Repository
AppState
AppState 負責儲存 App, View 的狀態,會以推播的方式將資料推送給 View。
View 狀態/變數 (Stateful)
Publisher 給 View 訂閱View 即是 SwiftUI.View,負責畫面顯示。
AppState 的資料,來監聽資料的變化Interactor
你說 Entities 在哪?直接開個 Model 資料夾定義在裡面即可。
理想上各模組間要以介面來隔開( Swift 裡的 Protocol )。
才能達到各模組間的解耦,只關注介面,不關注方法內的實作。
要在 SwiftUI 專案內實作 Clean Architecture,首先我們要先將資料夾劃分好:

EnvironmentObject
DependencyInjector.swift 主要的 EnvironmentObject,下方說明。AppState
AppState.swift 定義所有 AppState ProtocolInteractor
Interactor.swift 定義所有 Interactor ProtocolRepository
Repository.swift 定義所有 Repository ProtocolDecimal 的工具之類的Repository.swift先來定義 Repository.swift,把前幾章的 SQLite 方法定義成 Repository。
可以定義不同的 XXXRepository 來分類,最後聚合成一個 Repository。
這邊著重在定義出每個 CRUD,方便 Interactor 組合各種 CRUD 來對資料做操作:
import Foundation
protocol Repository: MemberRepository {}
protocol MemberRepository {
    func getMember(_ name: String?, _ position: Position?) throws -> [Member]
    func saveMember(_:Member) throws -> Member?
    func updateMember(_:Member) throws
    func deleteMember(_:Member) throws
}
這邊將存取
Member資料的方法都定義在MemberRepository
AppState.swift雖然說 AppState 應該定義成 Protocol(這邊應該要以 Combine 來達到可以根據 Keyword 來訂閱)
但小弟還沒研究出個所以然,所以就先直接實作了:
import Foundation
import Combine
struct AppState {
    var members = CurrentValueSubject<[Member]?, Never>([])
}
AppState儲存members這個狀態,讓View之後可以綁定使用這邊利用
Combine.CurrentValueSubject來達到類似@Published的效果有興趣可以自己去研究一下
CurrentValueSubject、PassthroughSubject,如果很多人敲碗我再來寫一篇 :P
Interactor.swiftInteractor 結構裡會有各式各樣的 XXXInteractor Protocol 變數。
跟 Repository 不同的是,Interactor 的方法都是要給 View 使用的,所以要以 View 的需求為出發。
另外資料都是以每個 Interactor 內的 AppState 傳遞的,所以這邊的方法也不用回傳資料:
import Foundation
struct Interactor {
    var member: MemberInteractor
}
protocol MemberInteractor {
    func getAllMember()
    func saveMember(_:Member)
    func updateMember(_:Member)
    func deleteMember(_:Member)
}
DependencyInjector.swiftDependencyInjector.swift 會實作一個 DIContainer 的 class。
主要是用於注入到 App 用的,裡面會包含所有 AppState 以及 Interactor:
import Foundation
struct DIContainer {
    // 為示範方便,這邊先暫時用 Optional 來定義
    var appState: AppState?
    var interactor: Interactor?
}
 
// 使 DIContainer 可以被當作環境變數注入
extension DIContainer: EnvironmentKey {
    static var defaultValue: DIContainer {
        return DIContainer()
    }
}
// 定義 DIContainer 的環境變數
extension EnvironmentValues {
    var injected: DIContainer {
            get { self[DIContainer.self] }
            set { self[DIContainer.self] = newValue }
        }
}
EnvironmentKey、EnvironmentValues兩個實作是讓下面的程式碼可以運作
最後就能將 DIContainer 注入進 App 內了:

import SwiftUI
@main
struct iThomeAppApp: App {
    private var container = DIContainer()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.injected, container)
        }
    }
}
如果要使用,只要在任何 View 前取得 EnvironmentKey 就可以了:

@Environment(\.injected) private var container: DIContainer
整個 SwiftUI Clean Architecture 是以 DIContainer 為包裝。
View 訂閱 DIContainer.AppState 的變數並顯示出來View 資料有變化時,調用 DIContainer.Interactor 來處理資料DIContainer.Interactor 處理資料,並與 Repository 互動來儲存資料DIContainer.Interactor 處理資料後將資料推播到 AppState
AppState 收到資料,儲存並推送給所有訂閱該資料的 View
View 接收到變化後的資料,改變顯示的狀態整個流程大概是這樣。
接下來的章節會詳細解釋每個模組該如何實作及使用。